package com.imooc.feeds.service;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.imooc.commons.constant.ApiConstant;
import com.imooc.commons.constant.PointTypesConstant;
import com.imooc.commons.exception.ParameterException;
import com.imooc.commons.model.domain.ResultInfo;
import com.imooc.commons.model.vo.SignInDinerInfo;
import com.imooc.commons.utils.AssertUtil;
import com.imooc.feeds.util.DateUtils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;

/**
 * \* @author: bear
 * \* Date: 2021-08-05-17:44
 * \* describe:
 */
@Service
public class SignService {

	@Value("${service.name.ms-oauth-server}")
	private String oauthServerName;
	@Value("${service.name.ms-diners-server}")
	private String dinersServerName;
	@Value("${service.name.ms-follow-server}")
	private String followServerName;
	@Value("${service.name.ms-points-server}")
	private String pointsServerName;

	@Resource
	private RedisTemplate redisTemplate;
	@Resource
	private RestTemplate restTemplate;

	/**
	 * 用户签到
	 *
	 * @param accessToken 登录用户 token
	 * @param dateStr 查询的日期，默认当天 yyyy-MM-dd
	 *
	 * @return 连续签到次数
	 */
	@Transactional(rollbackFor = Exception.class)
	public int doSign(String accessToken, String dateStr) {
		//获取用户登录信息
		SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
		//获取日期
		Date date = DateUtils.getDate(dateStr);

		//获取日期对应的天数，多少号
		int offset = DateUtil.dayOfMonth(date) - 1; //从0开始
		//构建key
		String signKey = buildSignKey(dinerInfo.getId(), date);
		//查看是否已经签到
		Boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);
		//签到
		redisTemplate.opsForValue().setBit(signKey, offset, true);
		//统计连续签到的次数
		int count = getContinuousSignCount(dinerInfo.getId(), date);
		//添加积分（可以使用消息队列进行性能优化）
		int points = addPoints(count,dinerInfo.getId());
		return points;
	}

	/**
	 * 统计某月连续签到次数
	 *
	 * @param dinerId 用户ID
	 * @param date 日期
	 *
	 * @return 当月连续签到次数
	 */
	private int getContinuousSignCount(Integer dinerId, Date date) {
		//获取日期对应的天数，多少号
		int dayOfMonth = DateUtil.dayOfMonth(date);
		//构建key
		String signKey = buildSignKey(dinerId, date);
		// 命令：bitfield key get [u/i]offset value
		// 此命令就是get取出key对应的位图，指定value索引位开始，取offset位偏移量的二进制
		BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
				.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0);
		List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
		if (list == null || list.isEmpty()) {
			return 0;
		}
		int signCount = 0;
		long v = list.get(0) == null ? 0 : list.get(0);
		// 取低位连续不为0的个数即为连续签到次数，需考虑当天尚未签到的情况
		for (int i = dayOfMonth; i > 0; i--) {// i 表示位移次数
			// 右移再左移，如果等于自己说明最低位是 0，表示未签到
			if (v >> 1 << 1 == v) {
				// 低位为 0 且非当天说明连续签到中断了
				if (i != dayOfMonth) {
					break;
				}
			} else {
				// 签到了 签到数加1
				signCount += 1;
			}
			// 右移一位并重新赋值，相当于把最右边一位去除
			v >>= 1;
		}
		return signCount;
	}

	/**
	 * 考虑到每月初需要重置连续签到次数，最简单的方式是按用户每月存一条签到数据（也可以每年存一条数据）。
	 * Key的格式为user:sign:uid:yyyyMM，Value则采用长度为4个字节（32位）的位图（最大月份只有31天）。
	 * 位图的每一位代表一天的签到，1表示已签，0表示未签。
	 * 构建存储Key user:sign:dinerId:yyyyMM
	 * e.g. user:sign:89:202011表示dinerId=89的食客在2020年11月的签到记录。
	 *
	 * @param dinerId
	 *
	 * @return
	 */
	private String buildSignKey(Integer dinerId, Date date) {
		return String.format("user:sign:%d%s", dinerId, DateUtil.format(date, "yyyyMM"));
	}

	private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
		//是否有token
		AssertUtil.mustLogin(accessToken);
		String url = oauthServerName + "/user/me?access_token={accessToken}";
		ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
		if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
			throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
		}
		//这里的data是一个linkedHashmap
		SignInDinerInfo dinerInfo = BeanUtil
				.fillBeanWithMap((LinkedHashMap)resultInfo.getData(), new SignInDinerInfo(), false);
		return dinerInfo;
	}

	/**
	 * 添加用户积分
	 *
	 * @param count 连续签到次数
	 * @param signInDinerId 登录人id
	 *
	 * @return
	 */
	public int addPoints(int count, Integer signInDinerId) {
		//签到1天送10积分，连续签到两天送20积分，3天送30积分，4天以上送50积分
		int point = 10;
		if (count == 2) {
			point = 20;
		} else if (count == 3) {
			point = 30;
		} else if (count >= 4) {
			point = 50;
		}
		//调用积分接口添加积分
		//构建请求头
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		//构建请求体，请求参数
		MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
		body.add("dinerId", signInDinerId);
		body.add("points", point);
		body.add("types", PointTypesConstant.sign.getType());
		HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
		//发送请求
		ResponseEntity<ResultInfo> result = restTemplate.postForEntity(pointsServerName+"/points", entity, ResultInfo.class);
		AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "积分新增失败");
		ResultInfo resultInfo = result.getBody();
		if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
			//失败了要回滚数据
			throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
		}
		return point;
	}
}
